package NeuronNetworkLibrary;

import java.util.ArrayList;

/**
 * This class represents the neural network.
 *
 * @author Zbyszko
 */
public class Network {

    /**
     * Number of neurons on following layers; i.e: {2,4,5} => first layer - 2
     * neurons, second one - 4 neurons and third one - 5 neurons.
     */
    private int neuronsInHiddenLayers[];
    /**
     * PL: W tablicy podajemy funkcje, jakie wywołujemy na danej ukrytej
     * warstwie Indeks każdej z nich odpowiada id-warstwy czyli:
     * neuronsInHiddenLayers[].length == activationFuncList[].length
     * -----------------------------------------------------------------------
     * ENG: In array, put the name of the function which will be used on given
     * layer. The index from this array is associated with the index from array
     * containg number of neurons, so: neuronsInHiddenLayers[].length ==
     * activationFuncList[].length
     */
    private String activationFuncList[];
    private String activationFuncOutput;
    /**
     * The interval of randomly taken weights
     */
    static public double MAX_WEIGHT = 0.5;
    static public double MIN_WEIGHT = -0.5;
    // ***********************************************************************
    private ArrayList<Neuron> inputLayer;
    private ArrayList<Neuron> outputLayer;
    private ArrayList<ArrayList<Neuron>> hiddenLayers;
    private ArrayList<Neuron> tempLayer;
    private double MSE;
    // PL: indeks wzorca, który wybieramy
    // ENG: index of pattern which is taken.
    private int patternNumber;
    private double[][] trainingSet;
    private double[][] desiredOutputs;
    private int numberOfInputNeurons;
    private int numberOfOutputNeurons;
    private int numberOfPatterns;
    public static boolean INIT_WEIGHTS_FLAG = true;
    // PL: tutaj tymczasowo przechowujemy wyjścia z metody calculatedOutput dla danej warstwy.
    // ENG: here, temporarily stores the outputs from calculatedOutput method fro given layer.
    private double[] tmp;

    /**
     * Creates a network with initial parameters.
     *
     * @param input
     * @param output
     * @param neuronsInHiddenLayer
     * @param activationFuncList
     * @param activationFuncOutput
     */
    public Network(double[][] input, double[][] output,
            int[] neuronsInHiddenLayer, String[] activationFuncList,
            String activationFuncOutput) {

        //System.out.println("Initializing neurons network...");
        this.trainingSet = input;
        this.desiredOutputs = output;
        this.neuronsInHiddenLayers = neuronsInHiddenLayer;
        this.activationFuncList = activationFuncList;
        this.activationFuncOutput = activationFuncOutput;

        this.numberOfInputNeurons = trainingSet[0].length;
        this.numberOfOutputNeurons = desiredOutputs[0].length;
        this.numberOfPatterns = trainingSet.length;

        inputLayer = new ArrayList<>();
        for (int i = 0; i < numberOfInputNeurons; i++) {
            inputLayer.add(new Neuron());
        }

        hiddenLayers = new ArrayList<>();
        for (int i = 0; i < neuronsInHiddenLayers.length; i++) {
            tempLayer = new ArrayList<>(neuronsInHiddenLayers[i]);

            for (int j = 0; j < neuronsInHiddenLayers[i]; j++) {
                tempLayer.add(new Neuron());
            }
            hiddenLayers.add(tempLayer);
        }

        outputLayer = new ArrayList<>();
        for (int i = 0; i < numberOfOutputNeurons; i++) {
            outputLayer.add(new Neuron());
        }
    }

    /**
     * Calculates the network.
     *
     * @param trainingSetNumber Index/Number of training set.
     */
    public void calculateNetwork(int trainingSetNumber) {

        this.patternNumber = trainingSetNumber;

        tmp = new double[numberOfInputNeurons+1]; //with bias
        for (int i = 0; i < numberOfInputNeurons; i++) {

            inputLayer.get(i).setInputNeuron(trainingSet[patternNumber][i]);
            //System.out.println(inputLayer.get(i).getCalculatedOutput());
            tmp[i] = inputLayer.get(i).getCalculatedOutput();
        }
        tmp[numberOfInputNeurons] = 1; // bias

        for (int m = 0; m < hiddenLayers.size(); m++) {
            for (int k = 0; k < hiddenLayers.get(m).size(); k++) {

                hiddenLayers.get(m).get(k).setHiddenNeuron(tmp, activationFuncList[m]);
                //System.out.println("ID: " + (m+1) + "." + (k+1) + "\n" + hiddenLayers.get(m).get(k));
            }
            tmp = new double[hiddenLayers.get(m).size()+1];
            for (int l = 0; l < tmp.length - 1; l++) {
                tmp[l] = hiddenLayers.get(m).get(l).getCalculatedOutput();
            }
            tmp[hiddenLayers.get(m).size()] = 1; //bias
        }


        for (int p = 0; p < numberOfOutputNeurons; p++) {
            outputLayer.get(p).setOutputNeuron(tmp, desiredOutputs[patternNumber][p], activationFuncOutput);
            //System.out.println("#" + (p) + "\n" + outputLayer.get(p) + "Delta: " /*+ outputLayer.get(p).getDelta()*/ + "\n");
        }

        this.calculateMSE(patternNumber);
        if (Network.INIT_WEIGHTS_FLAG) {
            Network.INIT_WEIGHTS_FLAG = false;
        }
    }

    /**
     * Returns Mean Square Error.
     *
     * @return
     */
    public double getMSE() {
        return MSE;
    }
       
    /**
     * Calculates Mean Square Error.
     *
     * @param n
     */
    private void calculateMSE(int n) {
        if (n == 0) {
            MSE = 0.0;
        }

        for (int i = 0; i < outputLayer.size(); i++) {
            MSE = MSE + outputLayer.get(i).getDelta() * outputLayer.get(i).getDelta();
        }

        MSE = MSE / (outputLayer.size() * numberOfPatterns);
    }
    
    /**
     * Calculate !Root! Mean Square Error.
     * @return RMSE
     */
    public double calculateRMSE(){
        double sum = 0;
        double diff, squared;
        
        for (int i = 0; i < this.outputLayer.size(); i++) {
            diff = this.outputLayer.get(i).getDesiredOutput() - this.outputLayer.get(i).getCalculatedOutput();
            squared = diff * diff;
            sum += squared;   
        }
        
        return Math.sqrt(sum / this.outputLayer.size());
    }

    /**
     * Returns the list of neurons on hidden layers.
     *
     * @return
     */
    public ArrayList<ArrayList<Neuron>> getHiddenLayers() {
        return hiddenLayers;
    }

    /**
     * Returns the list of neuron on output layer.
     *
     * @return
     */
    public ArrayList<Neuron> getOutputLayer() {
        return outputLayer;
    }

    /**
     * Returns the number of hidden layers.
     *
     * @return
     */
    public int getNumberOfHiddenLayers() {
        return neuronsInHiddenLayers.length;
    }

    /**
     * Returns the number of current pattern index.
     *
     * @return
     */
    public int getNumberOfPatterns() {
        return numberOfPatterns;
    }

    public void updateNumberOfPatterns() {
        this.numberOfPatterns = trainingSet.length;
    }

    public double[][] getTrainingSet() {
        return trainingSet;
    }

    public double[][] getDesiredOutputs() {
        return desiredOutputs;
    }

    public void setTrainingSet(double[][] trainingSet) {
        this.trainingSet = trainingSet;
    }

    public void setDesiredOutputs(double[][] desiredOutputs) {
        this.desiredOutputs = desiredOutputs;
    }

    public void setNeuronsInHiddenLayers(int[] neuronsInHiddenLayers) {
        this.neuronsInHiddenLayers = neuronsInHiddenLayers;
    }

    public int[] getNeuronsInHiddenLayers() {
        return neuronsInHiddenLayers;
    }
}